﻿/*                              
   Copyright 2011 Nils Kopal, Christopher Krüger, Simone Sauer, Dennis Nolte, Uni Duisburg-Essen

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Timers;
using System.Net;
using System.Collections;
using Sharebert.BootstrapServer.Properties;
using Sharebert.OverlayLibrary.Messages;
using Sharebert.OverlayLibrary;
using Sharebert.OverlayLibrary.API;
using Sharebert.LinkLayerLibrary;
using log4net;

namespace Sharebert.BootstrapServer
{   
    public class SharebertBootstrapServer
    {
		private ILog logger = LogManager.GetLogger(typeof(SharebertBootstrapServer));
		
        private static LinkedList<PeerEntry> peers = new LinkedList<PeerEntry>();

        private static object _lock = new object();

        /// <summary>
        /// Main entrance in our app
        /// </summary>
        /// <param name="args"></param>
        public static void Main(String[] args)
        {
            try
            {
                var server = new SharebertBootstrapServer();
                server.Start();
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.StackTrace);
            }
        }

        public SharebertBootstrapServer()
        {
        }

        /// <summary>
        /// Start the SharebertBootstrapServer
        /// </summary>
        private void Start()
        {
            Console.WriteLine("Welcome to Sharebert 3 Bootstrap Server\r\nTo stop this application please just close this window");
            Console.WriteLine("- Configured listen port is: " + Settings.Default.ListenPort);
            Console.WriteLine("- Configured check interval is: " + Settings.Default.CheckInterval);
            Console.WriteLine("- Configured delete interval is: " + Settings.Default.DeleteInterval);
            Console.WriteLine("Have fun...");

            var timer = new Timer();
            timer.Interval = Settings.Default.CheckInterval * 1000;
            timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
            timer.Start();

            var server = new BootstrapServer(IPAddress.Any, Settings.Default.ListenPort);
            server.ReadTimeout = Settings.Default.DeleteInterval;
            server.NeighbourRequestReceived += new EventHandler<PeerInfoEventArgs>(NeighbourRequestReceived);
            server.PeerIsOffline += new EventHandler<PeerInfoEventArgs>(PeerIsOffline);
            server.PingReceived += new EventHandler<PeerInfoEventArgs>(PingReceived);
            server.Start();
        }

        /// <summary>
        /// A peer wants to join the network
        /// - we store him in our list, if we do not know him
        /// - we tell him his successor and predecessor
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void NeighbourRequestReceived(object sender, PeerInfoEventArgs args)
        {
            var client = args.Client;
            var peerEntry = new PeerEntry(args.Peer.ID, client.RemoteIP, args.Peer.ListenPort);
			this.logger.Debug(String.Format("Received NeighbourRequest from {0}:{1} with ID: {2}", peerEntry.Peer.IP, peerEntry.Peer.ListenPort, peerEntry.Peer.ID));

            IPeer successor = null;
            IPeer predecessor = null;

            lock (_lock)
            {
                var query = from PeerEntry p in peers
                            where p.Peer.ID.Equals(peerEntry.Peer.ID)
                            select p;

                LinkedListNode<PeerEntry> peerNode = null;
                if (query.Count() == 0)
                {
                    // we do not know this peerID so we add him to our list
                    peerEntry.LastPingTime = DateTime.Now;
                    peerNode = InsertNewPeer(peerEntry);

                    this.logger.Info(String.Format("Added peer entry {0}:{1} | Active peers: {2}", client.RemoteIP, args.Peer.ListenPort, peers.Count));
                }
                else
                {
                    // we already know this peer so we refresh his timeout in our list:
                    foreach (var p in query)
                    {
                        if (!p.Peer.Equals(peerEntry.Peer))
                        {
                            try
                            {
                                // Not inserted, because the PeerID has already been taken
                                client.SendPeerIdInUse();
                                this.logger.Warn(String.Format("Peer {0}:{1} tried to register an existing PeerID | Active peers: {2}", peerEntry.Peer.IP, peerEntry.Peer.ListenPort, peers.Count));
                                return;
                            }
                            catch (NetworkException ex)
                            {
                                this.logger.Error(String.Format("Error while sending PeerIdInUse to client {0}:{1}!", client.RemoteIP, client.RemotePort), ex);
                                client.Disconnect();
                            }
                        }
                        else
                        {
                            p.LastPingTime = DateTime.Now;
                            peerNode = peers.Find(p);
                            this.logger.Info(String.Format("Refreshed peer entry {0}:{1} | Active peers: {2}", client.RemoteIP, args.Peer.ListenPort, peers.Count));
                        }
                    }
                }

                // finally we give the peer his successor and predecessor
                if (peers.Count > 1)
                {
                    if (peerNode.Previous != null)
                    {
                        // predecessor exists
                        predecessor = peerNode.Previous.Value.Peer;
                    }
                    else
                    {
                        // no direct predecessor
                        if (peers.Last != peerNode)
                        {
                            // use the last entry if its not the peer himself
                            predecessor = peers.Last.Value.Peer;
                        }
                    }
                    if (peerNode.Next != null)
                    {
                        // successor exists
                        successor = peerNode.Next.Value.Peer;
                    }
                    else
                    {
                        // no direct successor
                        if (peers.First != peerNode)
                        {
                            // use the first entry if its not the peer himself
                            successor = peers.First.Value.Peer;
                        }
                    }
                    this.logger.Debug(String.Format("Predecessor: {0} | New Peer: {1} | Successor: {2}", predecessor.ID, peerEntry.Peer.ID, successor.ID));
                }
            }

            var neighbours = new NeighbourList();
            if (predecessor != null)
            {
                neighbours.Predecessor = (Peer)predecessor;
            }
            if (successor != null)
            {
                neighbours.Successor = (Peer)successor;
            }

            try
            {
                client.SendNeighbours(neighbours);
                this.logger.Debug(String.Format("Sent Neighbours to {0}:{1}", client.RemoteIP, client.RemotePort));
                client.Disconnect();
            }
            catch (NetworkException ex)
            {
                this.logger.Error(String.Format("Error while sending neighbours to client {0}:{1}!", client.RemoteIP, client.RemotePort), ex);
            }
        }

        /// <summary>
        /// A peer pings us so we refresh him in our list
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void PingReceived(object sender, PeerInfoEventArgs args)
        {
            var client = args.Client;
            var peer = new PeerEntry(args.Peer.ID, client.RemoteIP, args.Peer.ListenPort);
            this.logger.Debug(String.Format("Ping from {0}:{1} received.", client.RemoteIP, args.Peer.ListenPort));

            try
            {
                lock (_lock)
                {
                    var query = from PeerEntry p in peers
                                where p.Peer.ID.Equals(peer.Peer.ID)
                                select p;

                    if (query.Count() == 0)
                    {
                        //maybe the peer was already deleted, so we add him again
                        peer.LastPingTime = DateTime.Now;
                        InsertNewPeer(peer);
                        this.logger.Info(String.Format("Added peer entry {0}:{1} | Active peers: {2}", client.RemoteIP, args.Peer.ListenPort, peers.Count));
                    }
                    else
                    {
                        foreach (var p in query)
                        {
                            p.LastPingTime = DateTime.Now;
                            this.logger.Info(String.Format("Refreshed peer entry {0}:{1} | Active peers: {2}", client.RemoteIP, args.Peer.ListenPort, peers.Count));
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                this.logger.Error("Exception in PingReceived:", ex);
            }
            finally
            {
                client.Disconnect();
            }
        }

        /// <summary>
        /// A peer tells us that he is going down, so we remove him from our list
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void PeerIsOffline(object sender, PeerInfoEventArgs args)
        {
            var client = args.Client;
            var peer = new PeerEntry(args.Peer.ID, client.RemoteIP, args.Peer.ListenPort);
            this.logger.Debug(String.Format("Received Offline from {0}:{1}", client.RemoteIP, client.RemotePort));

            try
            {
                lock (_lock)
                {
                    var query = from PeerEntry p in peers
                                where p.Peer.ID.Equals(peer.Peer.ID)
                                select p;

                    var markAsRemove = new LinkedList<PeerEntry>();

                    foreach (var p in query)
                    {
                        markAsRemove.AddLast(p);
                    }
                    foreach (var p in markAsRemove)
                    {
                        peers.Remove(p);
                        this.logger.Info(String.Format("Removed peer entry {0}:{1} (Offline) | Active peers: {2}", client.RemoteIP, client.RemotePort, peers.Count));
                    }
                }
            }
            catch (Exception ex)
            {
                this.logger.Error("Exception in PeerIsOffline:", ex);
            }
            finally
            {
                client.Disconnect();
            }
        }

        /// <summary>
        /// Check timer elapsed. So we check if a peers entry is too old and we have to remove him
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            try
            {
                lock (_lock)
                {
                    this.logger.Debug("Checking peer ages now");
                    var markAsRemove = new LinkedList<PeerEntry>();
                    var result = from PeerEntry peer in peers
                                 where (peer.LastPingTime.AddSeconds(Settings.Default.DeleteInterval) <= DateTime.Now)
                                 select peer;
                    foreach (var peer in result)
                    {
                        markAsRemove.AddLast(peer);
                    }
                    foreach (var peer in markAsRemove)
                    {
                        peers.Remove(peer);
                        this.logger.Info(String.Format("Removed peer entry {0}:{1} (Timed out) | Active peers: {2}", peer.Peer.IP, peer.Peer.ListenPort, peers.Count));
                    }
                }
            }
            catch (Exception ex)
            {
                this.logger.Error("Exception in timer_Elapsed!", ex);
            }
        }

        /// <summary>
        /// Inserts a new peer in the list.
        /// </summary>
        /// <param name="peer"></param>
        private LinkedListNode<PeerEntry> InsertNewPeer(PeerEntry peer)
        {
            if (peers.Count == 0)
            {
                return peers.AddFirst(peer);
            }
            else
            {
                // Check if the PeerID has already been taken
                var query = from PeerEntry p in peers
                            where p.Peer.ID.Equals(peer.Peer.ID)
                            select p;
                if (query.Count() > 0)
                {
                    return null;
                }

                foreach (var p in peers)
                {
                    // search for the first element with a higher PeerID and insert it before this element.
                    if (p.Peer.ID > peer.Peer.ID)
                    {
                        return peers.AddBefore(peers.Find(p), peer);
                    }
                }
                // no peer has a higher id, so insert it at the end.
                return peers.AddLast(peer);
            }
        }
    }
}
